using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading;
using System.Windows;
using System.Xml.Linq;
using MC.Phone.Ads.Provider;
using MC.Phone.Ads.Rx;
using MC.Phone.Ads.Utils;
using Microsoft.Phone.Net.NetworkInformation;
using Microsoft.Phone.Reactive;

namespace MC.Phone.Ads.Strategy
{
    public class RemoteAdProviderStrategy : DependencyObject, IAdProviderStrategy, INotifyPropertyChanged
    {
        private static readonly IDictionary<Uri, IEnumerable<AdCountryDescriptor>> ConfigCache = new Dictionary<Uri, IEnumerable<AdCountryDescriptor>>(); 
        private const string NoneAdProvider = "None";

        public static readonly DependencyProperty FallbackAdProviderNameProperty =
            DependencyProperty.Register("FallbackAdProviderName", typeof (string), typeof (RemoteAdProviderStrategy),
                                        null);

        public static readonly DependencyProperty LocalProviderSettingsUriProperty =
            DependencyProperty.Register("LocalProviderSettingsUri", typeof (Uri), typeof (RemoteAdProviderStrategy),
                                        null);

        public static readonly DependencyProperty RemoteProviderSettingsUriProperty =
            DependencyProperty.Register("RemoteProviderSettingsUri", typeof (Uri), typeof (RemoteAdProviderStrategy),
                                        null);

        public static DependencyProperty CacheConfigurationInMemoryProperty =
            DependencyProperty.Register("CacheConfigurationInMemory", typeof (bool), typeof (RemoteAdProviderStrategy), null);
        
        public bool CacheConfigurationInMemory
        {
            get { return (bool) GetValue(CacheConfigurationInMemoryProperty); }
            set { SetValue(CacheConfigurationInMemoryProperty, value); }
        }

        private readonly IAdProvider _noneAdProvider;

        public RemoteAdProviderStrategy()
        {
            _noneAdProvider = new NoneAdProvider();
        }

        [TypeConverter(typeof (UriTypeConverter))]
        public Uri LocalProviderSettingsUri
        {
            get { return (Uri) GetValue(LocalProviderSettingsUriProperty); }
            set { SetValue(LocalProviderSettingsUriProperty, value); }
        }

        [TypeConverter(typeof (UriTypeConverter))]
        public Uri RemoteProviderSettingsUri
        {
            get { return (Uri) GetValue(RemoteProviderSettingsUriProperty); }
            set { SetValue(RemoteProviderSettingsUriProperty, value); }
        }

        private IEnumerable<AdCountryDescriptor> CountryDescriptors { get; set; }

        private AdCountryDescriptor CurrentCountryDescriptor
        {
            get
            {
                if (CountryDescriptors.Any(d => d.Country == CountryCode))
                    return CountryDescriptors.First(d => d.Country == CountryCode);
                if (CountryDescriptors.Any(d => string.IsNullOrEmpty(d.Country)))
                    return CountryDescriptors.First(d => string.IsNullOrEmpty(d.Country));
                return CountryDescriptors.FirstOrDefault();
            }
        }

        #region IAdProviderStrategy Members

        public string CountryCode { set; private get; }

        public const string PausedPropertyName = "Paused";
        private bool _paused;

        public bool Paused
        {
            get { return _paused; }
            set
            {
                if (_paused != value)
                {
                    _paused = value;
                    RaisePropertyChanged(PausedPropertyName);
                }
            }
        }

        public WhenOffline WhenOffline
        {
            get; set; }

        private void RaisePropertyChanged(string propertyName)
        {
            if(PropertyChanged!=null)
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }

        public IEnumerable<IAdProvider> AdProviders { get; set; }

        public IAdProvider CurrentAdProvider { get; set; }

        public IObservable<IAdProvider> ProvideNext()
        {
            CachedFallbackAdProviderName = FallbackAdProviderName;
            Debug.WriteLine( Thread.CurrentThread.ManagedThreadId + "Provide Next");
            return new AnonymousObservable<IAdProvider>(
                observe =>
                    {
                        if (!DesignerProperties.IsInDesignTool)
                        {
                            bool doCache = CacheConfigurationInMemory;
                            if (doCache && CountryDescriptors == null && RemoteProviderSettingsUri != null && ConfigCache.ContainsKey(RemoteProviderSettingsUri))
                            {
                                CountryDescriptors = ConfigCache[RemoteProviderSettingsUri];
                            }
                            if (CountryDescriptors == null)
                            {
                                //First initialize CountryDescriptors
                                Debug.Assert((LocalProviderSettingsUri != null || RemoteProviderSettingsUri != null),
                                             "Please provide a Local or Remote Provider Settings Uri");
                                IObservable<MemoryStreamWithOrigin> settingsObservable = null;
                                Uri localUri = LocalProviderSettingsUri;
                                Uri remoteUri = RemoteProviderSettingsUri;
                                if (LocalProviderSettingsUri != null && RemoteProviderSettingsUri != null)
                                {
                                    IObservable<MemoryStreamWithOrigin> localProviderSettingsObservable =
                                        ObservableResource.FromResource(LocalProviderSettingsUri)
                                            .Select(stream => new MemoryStreamWithOrigin(stream, MemoryStreamWithOrigin.StreamOrigin.Local));
                                    IObservable<MemoryStreamWithOrigin> remoteProviderSettingsObservable =
                                        ObservableResource.FromInternet(RemoteProviderSettingsUri)
                                            .Select(stream => new MemoryStreamWithOrigin(stream, MemoryStreamWithOrigin.StreamOrigin.Remote));
                                    settingsObservable = remoteProviderSettingsObservable
                                        .Catch<MemoryStreamWithOrigin, Exception>(
                                            ex =>
                                                {
                                                    Debug.WriteLine( Thread.CurrentThread.ManagedThreadId + "WPUnifiedAd - Failed to load XML from: {0}",
                                                                    remoteUri);
                                                    return ObservableResource.FromCache("AdSettings.Xml")
                                                        .Select(stream => new MemoryStreamWithOrigin(stream, MemoryStreamWithOrigin.StreamOrigin.Cache));
                                                })
                                        .Catch<MemoryStreamWithOrigin, Exception>(
                                            ex =>
                                                {
                                                    Debug.WriteLine( Thread.CurrentThread.ManagedThreadId + "WPUnifiedAd - Failed to load XML from cache");
                                                    return localProviderSettingsObservable;
                                                })
                                        .Catch<MemoryStreamWithOrigin, Exception>(
                                            ex =>
                                                {
                                                    Debug.WriteLine( Thread.CurrentThread.ManagedThreadId + "WPUnifiedAd - Failed to load XML from: {0}",
                                                                    localUri);
                                                    return Observable.Return<MemoryStreamWithOrigin>(null);
                                                });
                                }
                                else if (LocalProviderSettingsUri != null)
                                {
                                    IObservable<MemoryStreamWithOrigin> localProviderSettingsObservable =
                                        ObservableResource.FromResource(LocalProviderSettingsUri)
                                            .Select(stream => new MemoryStreamWithOrigin(stream, MemoryStreamWithOrigin.StreamOrigin.Local));
                                    settingsObservable = localProviderSettingsObservable
                                        .Catch<MemoryStreamWithOrigin, Exception>(
                                            ex =>
                                                {
                                                    Debug.WriteLine( Thread.CurrentThread.ManagedThreadId + "WPUnifiedAd - Failed to load XML from: {0}",
                                                                    localUri);
                                                    return Observable.Return<MemoryStreamWithOrigin>(null);
                                                });
                                }
                                else if (RemoteProviderSettingsUri != null)
                                {
                                    IObservable<MemoryStreamWithOrigin> remoteProviderSettingsObservable =
                                        ObservableResource.FromInternet(RemoteProviderSettingsUri)
                                            .Select(stream => new MemoryStreamWithOrigin(stream, MemoryStreamWithOrigin.StreamOrigin.Remote));
                                    settingsObservable = remoteProviderSettingsObservable
                                        .Catch<MemoryStreamWithOrigin, Exception>(
                                            ex =>
                                                {
                                                    Debug.WriteLine( Thread.CurrentThread.ManagedThreadId + "WPUnifiedAd - Failed to load XML from: {0}",
                                                                    remoteUri);
                                                    return Observable.Return<MemoryStreamWithOrigin>(null);
                                                });
                                }
                                if (settingsObservable != null)
                                {
                                    settingsObservable.Select(
                                        mswo =>
                                            {
                                                if (mswo != null && mswo.Stream != null && mswo.Origin == MemoryStreamWithOrigin.StreamOrigin.Remote)
                                                {
                                                    ObservableResource.CacheResource(mswo.Stream,
                                                                                     "AdSettings.xml");
                                                    mswo.Stream.Position = 0;
                                                }
                                                return (mswo != null && mswo.Stream != null)
                                                           ? new StreamReader(mswo.Stream).ReadToEnd()
                                                           : string.Empty;
                                            })
                                        .Subscribe(
                                            xmlString =>
                                                {
                                                    CountryDescriptors = ParseXml(xmlString).ToList();
                                                    if (doCache && remoteUri != null && ConfigCache.ContainsKey(remoteUri) == false)
                                                    {
                                                        ConfigCache.Add(remoteUri, CountryDescriptors);
                                                    }
                                                    Debug.WriteLine(Thread.CurrentThread.ManagedThreadId + " settingsObservable.Select");
                                                    SelectNextRandomAdProvider(observe);
                                                });
                                }
                            }
                            else
                            {
                                Debug.WriteLine(Thread.CurrentThread.ManagedThreadId + " with countrydescriptors");
                                SelectNextRandomAdProvider(observe);
                            }
                        }
                        return Disposable.Empty;
                    }).SubscribeOnDispatcher();
        }


        public string FallbackAdProviderName
        {
            get { return (string) GetValue(FallbackAdProviderNameProperty); }
            set { SetValue(FallbackAdProviderNameProperty, value); }
        }

        private string CachedFallbackAdProviderName { get; set; }

        #endregion

        private void SelectNextRandomAdProvider(IObserver<IAdProvider> observer, AdEventArgs adSelectionCause = null)
        {
            bool hasNetwork = true;
            if (WhenOffline != WhenOffline.ShowAds)
            {
                hasNetwork = NetworkHelper.NetworkAvailable;
            }
            if (!Paused)
            {
                string adProviderName = null;
                if(!hasNetwork)
                {
                    Debug.WriteLine( Thread.CurrentThread.ManagedThreadId + "WPUnifiedAd - Offline");
                    if (WhenOffline == WhenOffline.UseFallbackAdProvider)
                        adProviderName = CachedFallbackAdProviderName;
                }
                else
                {
                    Debug.WriteLine( Thread.CurrentThread.ManagedThreadId + "WPUnifiedAd - Online");
                    adProviderName =
                        CurrentCountryDescriptor.FindAdProviderName(AdProviders.Select(ap => ap.Name).Union(new[] {"None"}),
                                                                adSelectionCause);
                }
                if (!string.IsNullOrEmpty(adProviderName))
                {
                    Debug.WriteLine( Thread.CurrentThread.ManagedThreadId + "WPUnifiedAd - Chosen adprovider: {0}", adProviderName);
                    IAdProvider adProvider;
                    if (adProviderName == NoneAdProvider || !AdProviders.Any(ap => ap.Name == adProviderName))
                        adProvider = _noneAdProvider;
                    else
                        adProvider = AdProviders.Single(ap => ap.Name == adProviderName);

                    Observable.FromEvent<AdEventArgs>(adProvider, "NoAd")
                        .Take(1)
                        .ObserveOn(Scheduler.ThreadPool)
                        .Subscribe(
                            e => SelectNextRandomAdProvider(observer, e.EventArgs));

                    CurrentAdProvider = adProvider;
                    observer.OnNext(adProvider);
                }
                else
                {
                    Observable.FromEvent<NetworkNotificationEventArgs>(
                        addHandler => DeviceNetworkInformation.NetworkAvailabilityChanged += addHandler,
                        removeHandler => DeviceNetworkInformation.NetworkAvailabilityChanged -= removeHandler)
                        .Where(e=> (NetworkHelper.NetworkAvailable))
                        .Subscribe(e=>
                        
                                       {
                                           Debug.WriteLine( Thread.CurrentThread.ManagedThreadId + "WPUnifiedAd - Network came up again.");
                                           SelectNextRandomAdProvider(observer);
                                       });

                }
            }
            else
            {
                Debug.WriteLine( Thread.CurrentThread.ManagedThreadId + "WPUnifiedAd - AdProviderStrategy paused.");
                Observable.FromEvent<PropertyChangedEventArgs>(this, "PropertyChanged")
                    .Where(e => e.EventArgs.PropertyName == "Paused")
                    .Where(e => !Paused)
                    .Take(1)
                    .Subscribe(
                        e =>
                            {
                                Debug.WriteLine( Thread.CurrentThread.ManagedThreadId + "WPUnifiedAd - AdProviderStrategy unpaused.");
                                SelectNextRandomAdProvider(observer);
                            });

            }
        }

        private IEnumerable<AdCountryDescriptor> ParseXml(string xml)
        {
            if (string.IsNullOrEmpty(xml))
            {
                Debug.WriteLine( Thread.CurrentThread.ManagedThreadId + "There's no xml to parse so no AdProviders.");
                return new List<AdCountryDescriptor>
                           {
                               new AdCountryDescriptor
                                   {
                                       Probabilities =
                                           new List<Probability> {new Probability {ProviderName = "None", Value = 1}}
                                   }
                           };
            }
            XDocument doc = XDocument.Parse(xml);
            IEnumerable<AdCountryDescriptor> countryDescriptors =
                doc.Descendants("AdCountryDescriptor")
                    .SelectMany(
                        cd =>
                            {
                                string countries = cd.Attribute("Country") != null
                                                       ? cd.Attribute("Country").Value
                                                       : string.Empty;
                                string initialProvider = cd.Attribute("InitialAdProvider") != null
                                                             ? cd.Attribute("InitialAdProvider").Value
                                                             : null;
                                return countries.Split(',', ';')
                                    .Select(c => c.Trim())
                                    .Select(
                                        country =>
                                        new AdCountryDescriptor
                                            {
                                                InitialProviderName = initialProvider,
                                                Country = country,
                                                Probabilities =
                                                    cd
                                                    .Descendants("Probability")
                                                    .Select(p =>
                                                            new Probability
                                                                {
                                                                    ProviderName =
                                                                        p.Attribute("AdProvider").Value,
                                                                    Value =
                                                                        double.Parse(p.Attribute("Value").Value),
                                                                }).ToList()
                                            });
                            });
            XNamespace unifiedNameSpace = "http://wpunifiedad.codeplex.com";
            IEnumerable<AdCountryDescriptor> countryDescriptorsFromXmlWithSchema =
                doc.Descendants(unifiedNameSpace+"AdCountryDescriptor")
                    .SelectMany(
                        cd =>
                            {
                                string countries = cd.Attribute("Country") != null
                                                       ? cd.Attribute("Country").Value
                                                       : string.Empty;
                                string initialProvider = cd.Attribute("InitialAdProvider") != null
                                                             ? cd.Attribute("InitialAdProvider").Value
                                                             : null;
                                return countries.Split(',', ';')
                                    .Select(c => c.Trim())
                                    .Select(
                                        country =>
                                        new AdCountryDescriptor
                                            {
                                                InitialProviderName = initialProvider,
                                                Country = country,
                                                Probabilities =
                                                    cd
                                                    .Descendants(unifiedNameSpace + "Probability")
                                                    .Select(p =>
                                                            new Probability
                                                                {
                                                                    ProviderName =
                                                                        p.Attribute("AdProvider").Value,
                                                                    Value =
                                                                        double.Parse(p.Attribute("Value").Value),
                                                                }).ToList()
                                            });
                            }).ToList();

            IEnumerable<AdCountryDescriptor> wp7AdRotatorDescriptors =
                doc.Descendants("AdCultureDescriptor")
                    .Select(cd =>
                            new AdCountryDescriptor
                                {
                                    Country = cd.Attribute("CultureName").Value != "default"
                                                  ? CultureToCountry(cd.Attribute("CultureName").Value)
                                                  : string.Empty,
                                    Probabilities = cd
                                        .Descendants("Probabilities")
                                        .Select(p =>
                                                new Probability
                                                    {
                                                        ProviderName = p.Attribute("AdType").Value,
                                                        Value = double.Parse(p.Attribute("Probability").Value),
                                                    }).ToList()
                                });

            return countryDescriptors.Union(countryDescriptorsFromXmlWithSchema).Union(wp7AdRotatorDescriptors);
        }

        private string CultureToCountry(string culture)
        {
            var cultureInfo = new CultureInfo(culture);
            return cultureInfo.GetCountryCode();
        }

        #region Nested type: AdCountryDescriptor

        private class AdCountryDescriptor
        {
            private static readonly Random Rnd = new Random();
            private int _providerNamesProvided;

            public AdCountryDescriptor()
            {
                Probabilities = new List<Probability>();
            }

            public string Country { get; set; }
            public string InitialProviderName { get; set; }
            public IList<Probability> Probabilities { get; set; }

            public string FindAdProviderName(IEnumerable<string> validAdProviders, AdEventArgs adSelectionCause)
            {
                validAdProviders = validAdProviders.ToList();
                if (adSelectionCause != null)
                {
                    //Add penalty to the AdProvider
                    Probability probability =
                        Probabilities.SingleOrDefault(p => p.ProviderName == adSelectionCause.AdProviderName);
                    if (probability != null)
                    {
                        probability.NoAdCount++;
                        probability.LastNoAdOn = DateTime.Now;
                    }
                }
                if (!string.IsNullOrEmpty(InitialProviderName) && _providerNamesProvided == 0)
                {
                    if (validAdProviders.Contains(InitialProviderName))
                    {
                        _providerNamesProvided++;
                        return InitialProviderName;
                    }
                    Debug.WriteLine( Thread.CurrentThread.ManagedThreadId + 
                        string.Format("WPUnifiedAd - Provided InitialProviderName '{0}' not a configured AdProvider.",
                                      InitialProviderName));
                }
                var validGroupedProbabilities =
                    Probabilities
                        .Where(p => p.LastNoAdOn.AddMinutes(1) < DateTime.Now || p.ProviderName == NoneAdProvider)
                        .Where(p => validAdProviders.Contains(p.ProviderName))
                        .GroupBy(p => p.NoAdCount)
                        .OrderBy(p => p.Key);
                List<Probability> validProbabilities;
                if(validGroupedProbabilities.Any())
                {
                    validProbabilities = validGroupedProbabilities.First().ToList();
                }
                else
                {
                    //No valid probabilities left. Fall back to None
                    validProbabilities = new List<Probability>( new[]{new Probability {ProviderName = NoneAdProvider}});
                }
                if (!validAdProviders.Any())
                {
                    _providerNamesProvided++;
                    return NoneAdProvider;
                }
                double totalValueBetweenValidAds = validProbabilities.Sum(p => p.Value);
                double randomValue = Rnd.NextDouble()*totalValueBetweenValidAds;
                double totalCounter = 0;
                foreach (Probability probability in validProbabilities)
                {
                    totalCounter += probability.Value;
                    if (randomValue < totalCounter)
                    {
                        _providerNamesProvided++;
                        return probability.ProviderName;
                    }
                }
                _providerNamesProvided++;
                return NoneAdProvider;
            }
        }

        #endregion

        #region Nested type: Probability

        private class Probability
        {
            public double Value { get; set; }
            public string ProviderName { get; set; }
            public int NoAdCount { get; set; }
            public DateTime LastNoAdOn { get; set; }
        }

        #endregion

        public event PropertyChangedEventHandler PropertyChanged;
    }
}